package com.example.study.juejin.android修炼

/**
 *  created by 20300
 *   on 2022/3/22
 *   describe:https://juejin.cn/post/6935358323139018766
 */
class Class类加载过程与类加载器 {
/*
java虚拟机为了保证实现语言的无关性,试讲虚拟机只与"class"文件"字节码"这种特定
形式的二进制文件格式相关联,而不是与实现语言绑定

类加载过程
Class类从被加载到虚拟机内存开始,到卸载出内存为止,其生命周期包括:
加载(Loading),验证(verification),准备(Preparation),解析(Resolution),
初始化(Initialization),使用(Using),卸载(Unloading)七个阶段
    图在链接

加载阶段
其中累的全限定名是Class文件(Java由编译器自动生成)内的代表常量池内的16进制值所代表
的特定符号引用.因为Class文件格式有其自己的一套规范,如第1-4代表魔数(???),第5-6节
代表次版本,第7-8字节代表主版本号
    图在链接:
    在加载阶段虚拟机会通过类的全限定名来获取该类的二进制流
    |
    将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
    |
    内存中生成的一个代表这个类的java.lang.class对象,作为方法区内该类的
    各数据的访问入口
简单来说,虚拟机不关心这种"特定二进制流"从哪里来的,从本地加载也好,网上下载的也罢,
都没关系.虚拟机要做的就是将该二进制流写在自己的内存中并生成相应的Class对象(并不是在堆中)
这个阶段,我们能够通过我们自定义类加载器来控制二进制流的获取方式.

验证阶段
验证阶段,正因为加载阶段虚拟机不介意二进制大的来源,所以就可能存在着影响虚拟机正常运行的
安全隐患.所以虚拟机对于该二进制的校验工作非常重要.校验方式包括但不限于:
    文件格式校验:产看该二进制文件的格式是否符合Class文件格式标准.如上面提到的每个字节
        是否都有对应的有效值
    元数据校验:查看字节码描述的语义信息是否符合java语言规范
    符号引用校验:是对类自身以外的信息进行匹配性校验.如通过符号引用中的类全限定名是否能找到对应的类,
        类中是否能找到对应的类,类中是否存在符合方法的字段描述符等等
    字节码校验:通过数据流和控制流分析,确定程序的语义是否合法并符合逻辑

准备阶段
准备阶段在此阶段将正式为类变量分配内存并设置变量的初始化值.注意的是,类变量是指static的静态变量,
是分配在方法区之中的,而不是像对象变量,分配在堆中.还有一点需要注意,final常量在此阶段就已经被赋值了.
如下:
    public static int SIZE = 10;//初始化值 == 0
    public static final int SIZE = 10;//初始化值 ==10

解析阶段
解析阶段是将常量池内的符号引用替换为直接引用的过程,.符号引用就是上文说的Class文件格式标准所规定的特定
字面量,而直接引用就是我们说的指针,内存引用等概念

初始化阶段
到了初始化阶段,就开始真正执行我们的字节码程序了.也可以理解成:类初始化阶段就是虚拟机内部执行类构造<clinit>
()方法的过程.注意,这个类构造方法可不是虚拟机内部生成的,而是我们的编译器自动生成的,是编译器自动收集类中的
所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,具体分析见下.
注意,这里说的是类变量复制动作,即Static并且具有赋值操作,如果无赋值操作,那么在尊卑阶段进行的方法区初始化就算
完成了.为何还要加上桑static{}呢,我们可以把Static{}理解成:是由多个静态初始化动作组织成一个特殊的"静态子句",
与其他的静态初始化动作一样.这也是为何static{}只会执行一遍并在对象构造方法之前执行的原因.如下代码:
public class Tested{
public static int T;//无赋值,不在类构造中再次初始化
public  int c = 1;//不会在类构造中
static{
T = 10;
}}
还有一点,编辑器手机类变量的顺序,也就是虚拟机在此初始化阶段的执行顺序,这个顺序就是变量在类中
语句定义的先后顺序,如上面的:语句2:T在6:T之前,这是两个独立的语句.类构造<clinit>的其他特点如下:
    类构造方法,不需要显示调用父类构造器,虚拟机会保证在执行子类构造之前,父类的构造已经执行完毕

编译期的<clinit>
我们将流程回溯到编译期阶段,以刚刚的Tested类代码为例.通过javap-c/Tested.class(注意:
/../Tested绝对路径),获取Class文件:
public class com.tencent.lo.Tested {
  public static int T;

  public int c;

  public com.tencent.lo.Tested();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_1
       6: putfield      #2                  // Field c:I
       9: return

  static {};
    Code:
       0: bipush        10
       2: putstatic     #3                  // Field T:I
       5: return
}
在Class文件中我们能明显的看到invokespecial对应的对象构造<init>:()V,那为什么没有看到<clinit>类构造方法呢
其实上面的static{}就是.看下OpenJDK源码的Constants接口,此接口定义了在编译器中所用到的常量,这是一个自动生成的类
public interface Constants extends RuntimeConstants {
    public static final boolean tracing = true;

    Identifier idClassInit = Identifier.lookup("<clinit>");
    Identifier idInit = Identifier.lookup("<init>");
}
在MemberDefinition类中,判断是否为类构造器字符:
    public final boolean isInitializer() {
        return getName().equals(idClassInit); // 类构造
    }
    public final boolean isConstructor() {
        return getName().equals(idInit); // 对象构造
    }
而在MemberDefinition的toString()方法中,我们能够看到,当类构造时,会输出特定字符,而不会像对象构造那样输出规范的
字符串.
    public String toString() {
        Identifier name = getClassDefinition().getName();
        if (isInitializer()) { // 类构造
            return isStatic() ? "static {}" : "instance {}";
        } else if (isConstructor()) { // 对象构造
            StringBuffer buf = new StringBuffer();
            buf.append(name);
            buf.append('(');
            Type argTypes[] = getType().getArgumentTypes();
            for (int i = 0 ; i < argTypes.length ; i++) {
                if (i > 0) {
                    buf.append(',');
                    }
                buf.append(argTypes[i].toString());
                }
            buf.append(')');
            return buf.toString();
        } else if (isInnerClass()) {
            return getInnerClass().toString();
        }
        return type.typeString(getName().toString());
    }

类加载器
"虚拟机将类加载阶段中的"通过一个全限定名来获取描述此类的二进制字节流"这个动作放到了外部来实现,以便开发者可以自己
决定如何获取所需的类文件,而实现这个动作的代码木块就被称为类加载器.对于任意一个类来说,只有在类加载器相同的情况下比较
两者是否相同才有意义,否则即使是同个文件,在不同加载器下,在虚拟机看来其仍然是不同的,是两个独立到的类.
我们可以将类加载器分为三类:
    启动类加载器:
    负责加载<JAVA_HOME>\lib目录下,或被-Xbootclasspath参数指定路径的,并能够被虚拟机识别的类库加载到虚拟机内存中
    扩展类加载器:
    负责加载<JAVA_HOME>\lib\ext目录下,或被java.ext.dirs系统变量所在指定的类库.能够被开发者直接使用
    应用程序加载器:
    是我们常说的系统类加载器,即ClassLoader中getSystemClassLoader()方法的返回值,那么开发者可以直接使用,如果应用程序
    中没有自定义过类加载器,那么其就是程序默认的加载器

双亲委派
而所谓的双亲委派模型就是:"如果一个类加载其收到类加载的请求,它首先不会自己去尝试加载这个类,而是把加载的操作委托给
父类加载器去完成,每一层次加载器都是如此,因此所有的加载请求都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法
完成这个加载请求时(他的搜索范围没有找到所需的类,因为上面所说的启动类加载器和扩展类加载器,只能加载特定目录之下的,或被
-x参数所指定的类库),子类才会尝试自己加载".注意这里说的父类只是层次结构,其并不是直接继承关系,而是通过组合方式来复用
父类的加载器的
"双亲委派的好处就是,使加载器也具备了优先级的层次结构.例如,java.lang.Object存放在<JAVA_HOME>/lib下的rt.jar包内,
无论哪个类加载器要加载这个类,最终都会委派给最顶层的启动类加载器,所以保证了Object类在各类加载器环境中都是同一个类.
相反,如果没有双亲委派模型,如果用户编写了一个java.lang.Object,并放在程序的ClassPath下,那么系统将会出现多个不同的
Object类
为何?因为每个加载器各自为政,不会委托给父构造器,如上面所说,只要加载器不同,即使类Class文件相同,其也是独立的.
试想如果你自己在项目中编写了一个java.lang.Object类(当然不能放入rt.jar类库中替换掉同名Object文件,这样做没有意义,
如果虚拟机加载校验能通过,相当于改了源码),我们通过自定义的构造器来加载这个类可以吗?理论上来说,虽然着两个类都是java.lang
.Object,但由于构造器不同,对于虚拟机来说这是不同的Class文件,当然可以,但是实际上呢
    public void loadPathName(String classPath) throws ClassNotFoundException {
        new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                InputStream is = getClass().getResourceAsStream(name);
                if (is == null)
                    return super.loadClass(name);
                byte[] b;
                try {
                    b = new byte[is.available()];
                    is.read(b);
                } catch (Exception e) {
                    return super.loadClass(name);
                }
                return defineClass(name, b, 0, b.length);
            }
        }.loadClass(classPath);
    }
实际的逻辑是defineClass方法.可以发现,自定义加载器是无法加载以java开头的系统类的.
    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
            throws ClassFormatError {

        protectionDomain = preDefineClass(name, protectionDomain);
        ... // 略

        return c;
    }

    private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);
        // 在这里能看到系统类，自定义的加载器是不能加载的
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                    ("Prohibited package name: " +
                            name.substring(0, name.lastIndexOf('.')));
        }
        ... // 略

        return pd;
    }
如果你用AS直接查看,会发现,defineClass内部是没有具体实现的,源码见下.可这并不代表android的defineClass方法
实现与java不同,因为都是引用的java.lang包下的ClassLoader类,逻辑肯定都是一样的.之所以看到的源码不一样,这是由于
SDK和JAVA源码包的区别导致的.SDK内的源码是谷歌提供给我们方便开发查看的,并不完全等同于源码
    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        throw new UnsupportedOperationException("can't load this type of class file");
    }


*/

}